#pragma once

#include <string>
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>

#ifdef __unix__
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

struct SocketMessage {
    virtual ~SocketMessage () = default;
    virtual std::vector<char> encode () const = 0;
};

struct SocketClient {

    using msg_t = std::shared_ptr<SocketMessage>;

    SocketClient (const std::string &ip, int port) {
        sock_fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (sock_fd < 0) {
            printf("failed to create socket.\n");
            return;
        }
        sockaddr_in addr = {};
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &addr.sin_addr);
        if (::connect(sock_fd, (sockaddr*)&addr, sizeof(addr)) < 0) {
            printf("failed to connect to server.\n");
            close();
            return;
        }
        worker = std::thread([this] () {
            for (;;) {
                msg_t msg; {
                    std::unique_lock<std::mutex> lock(mutex);
                    cond.wait(lock);
                    if (!queue.empty()) {
                        msg = queue.back();
                        queue.pop();
                    }
                }
                if (!msg)
                    break;
                auto payload = msg->encode();
                auto data = payload.data();
                auto size = payload.size();
                if (::send(sock_fd, data, size, 0) < 0) {
                    printf("failed to send message: %s\n", strerror(errno));
                    close();
                    break;
                }
                if (size > 128) {
                    std::string header(data, 64);
                    std::string footer(data + size - 64, 64);
                    printf("sent: (%zd bytes) %s ... %s\n", size, header.c_str(), footer.c_str());
                } else {
                    std::string content(data, size);
                    printf("sent: %s\n", content.c_str());
                }
            }
        });
    }

    ~SocketClient () {
        {
            std::unique_lock<std::mutex> lock(mutex);
            cond.notify_all();
        }
        worker.join();
        close();
    }

    bool valid () const {
        return sock_fd >= 0;
    }

    void write (msg_t msg) {
        std::unique_lock<std::mutex> lock(mutex);
        queue.emplace(std::forward<msg_t>(msg));
        cond.notify_one();
    }

    std::string readLine () {
        std::string res;
        char buf[4096];
        for (;;) {
            auto size = ::recv(sock_fd, buf, 4096, 0);
            if (size < 0)
                break;
            if (buf[size - 1] == '\n') {
                res.append(buf, size - 1);
                break;
            }
            res.append(buf, size);
        }
        return res;
    }

    void close () {
        if (sock_fd >= 0) {
            ::close(sock_fd);
            sock_fd = -1;
        }
    }

private:
    int sock_fd;
    std::thread worker;
    std::mutex mutex;
    std::condition_variable cond;
    std::queue<msg_t> queue;
};


#else



#endif